package com.fuyun.microservice.codec;

import com.fuyun.microservice.codec.msg.LoginResponseMessage;
import com.fuyun.microservice.utils.BufferUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.TooLongFrameException;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
public class FrameDecoder extends ByteToMessageDecoder {
    private static final int MAX_FRAMELENGTH = 1024 * 8;
    private static final int LENGTH_BYTES = 2;
    private boolean failFast = true;
    private long tooLongFrameLength;
    private boolean discardingTooLongFrame;
    private long bytesToDiscard;
    private ChannelHandlerContext channelHandlerContext;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        channelHandlerContext=ctx;
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }


    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
        if (discardingTooLongFrame) {
            discardingTooLongFrame(in);
        }

        if (in.readableBytes() < LENGTH_BYTES) {
            return null;
        }

        int frameLength = in.readShort();
        log.debug("Frame Length:{}", frameLength);

        if (frameLength == 0) {
            return null;
        }

        if (frameLength < 0 ) {
            failOnNegativeLength(in, frameLength);
        }

        if (frameLength > MAX_FRAMELENGTH) {
            exceededFrameLength(in, frameLength);
            return null;
        }

        int contentLength = frameLength - HeaderCodec.HEADER_LENGTH;
        if(contentLength <0){
            log.debug("content length nagative:"+contentLength);
            return null;
        }

        //包不完整，10=CommandId Length + SeqNo Length
        if (in.readableBytes() < contentLength+10) {
            failOnFrameLengthLargerThanReadable(in,frameLength,in.readableBytes());
            return null;
        }

        short commandId = in.readShort();
        if (checkCommand(commandId) == false) {
            failOnUnCorrectCommand(in, commandId,frameLength);
        }

        int readerIndex = in.readerIndex() - 4;
        ByteBuf frame = extractFrame(in, readerIndex, frameLength);

        log.debug("RECEIVED: {}B\n{}", frame.readableBytes(),ByteBufUtil.prettyHexDump(frame));
        in.readerIndex(readerIndex + frameLength);
        return frame;
    }


    private boolean checkCommand(Short commandId) {
        for (PacketType pt : PacketType.values()) {
            if (pt.getCommandId() == commandId) {
                return true;
            }
        }
        return false;
    }

    private void failOnUnCorrectCommand(ByteBuf in, short commandId,long frameLength) {
        triggerException("command error "+commandId);
        in.skipBytes((int)frameLength-4);
        throw new CorruptedFrameException(
                "wrong commandId: " + commandId);
    }


    private void failOnNegativeLength(ByteBuf in, long frameLength) {
        triggerException("negative body length "+frameLength);
        throw new CorruptedFrameException(
                "negative pre-adjustment body length: " + frameLength);
    }


    private void failOnFrameLengthLargerThanReadable(ByteBuf in, long frameLength,int readableBytes) {
        in.readerIndex(in.readerIndex() -LENGTH_BYTES);
        log.debug( "Adjusted frame length (" + readableBytes + ") is less " +
                "than lengthFieldEndOffset: " +  readableBytes);
    }


    private void exceededFrameLength(ByteBuf in, long frameLength) {
        triggerException("body length exceeded");
        long discard = frameLength- LENGTH_BYTES - in.readableBytes();
        tooLongFrameLength = frameLength;

        if (discard < 0L) {
            // buffer contains more bytes then the frameLength so we can discard all now
            in.skipBytes((int)frameLength-LENGTH_BYTES);
        } else {
            // Enter the discard mode and discard everything received so far.
            discardingTooLongFrame = true;
            bytesToDiscard = discard;
            in.skipBytes(in.readableBytes());
        }

        failIfNecessary(true);
    }

    private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
        if (bytesToDiscard == 0) {
            // Reset to the initial state and tell the handlers that the frame was too large.
            long tooLongFrameLength = this.tooLongFrameLength;
            this.tooLongFrameLength = 0;
            discardingTooLongFrame = false;
            if (!failFast || firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        } else {
            // Keep discarding and notify handlers if necessary.
            if (failFast && firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        }
    }


    private void discardingTooLongFrame(ByteBuf in) {
        long bytesToDiscard = this.bytesToDiscard;
        int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
        in.skipBytes(localBytesToDiscard);
        bytesToDiscard -= localBytesToDiscard;
        this.bytesToDiscard = bytesToDiscard;

        failIfNecessary(false);
    }

    private void fail(long frameLength) {
        if (frameLength > 0) {
            throw new TooLongFrameException(
                    "Adjusted frame length exceeds " + MAX_FRAMELENGTH +
                            ": " + frameLength + " - discarded");
        } else {
            throw new TooLongFrameException(
                    "Adjusted frame length exceeds " + MAX_FRAMELENGTH +
                            " - discarding");
        }
    }

    private void triggerException(String errorMsg){
        LoginResponseMessage loginResponseMessage=new LoginResponseMessage(BufferUtil.getSequenceNo());
        loginResponseMessage.setRspCode(-1);
        loginResponseMessage.setRspMessage(errorMsg);
        channelHandlerContext.channel().writeAndFlush(loginResponseMessage);
    }

    protected ByteBuf extractFrame(ByteBuf buffer, int index, int length) {
        return buffer.retainedSlice(index, length);
    }

}
